You may want to pause your Python code to wait for an update from a widget. Typically this would be hard to do since running Python code blocks any widget messages from the frontend until the Python code is done.
We'll do this in two approaches: using the event loop integration, and using plain generator functions.
First we register a new event loop integration. This registration will not be needed once https://github.com/ipython/ipykernel/pull/246 is merged and released.
In [ ]:
import asyncio
from ipykernel.eventloops import register_integration
@register_integration('asyncio')
def loop_asyncio(kernel):
'''Start a kernel with asyncio event loop support.'''
loop = asyncio.get_event_loop()
def kernel_handler():
loop.call_soon(kernel.do_one_iteration)
loop.call_later(kernel._poll_interval, kernel_handler)
loop.call_soon(kernel_handler)
try:
if not loop.is_running():
loop.run_forever()
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
Next we invoke our asyncio event loop.
In [ ]:
%gui asyncio
We define a new function that returns a future for when a widget attribute changes.
In [ ]:
import asyncio
def wait_for_change(widget, value):
future = asyncio.Future()
def getvalue(change):
# make the new value available
future.set_result(change.new)
widget.unobserve(getvalue, value)
widget.observe(getvalue, value)
return future
And we finally get to our function where we will wait for widget changes. We'll do 10 units of work, and pause after each one until we observe a change in the widget. Notice that the widget's value is available to us, since it is what the wait_for_change
future has as a result.
Run this function, and change the slider 10 times.
In [ ]:
from ipywidgets import IntSlider
slider = IntSlider()
async def f():
for i in range(10):
print('did work %s'%i)
x = await wait_for_change(slider, 'value')
print('async function continued with value %s'%x)
asyncio.ensure_future(f())
slider
First, we define a decorator which hooks a generator function up to widget change events.
In [ ]:
from functools import wraps
def yield_for_change(widget, attribute):
"""Pause a generator to wait for a widget change event.
This is a decorator for a generator function which pauses the generator on yield
until the given widget attribute changes. The new value of the attribute is
sent to the generator and is the value of the yield.
"""
def f(iterator):
@wraps(iterator)
def inner():
i = iterator()
def next_i(change):
try:
i.send(change.new)
except StopIteration as e:
widget.unobserve(next_i, attribute)
widget.observe(next_i, attribute)
# start the generator
next(i)
return inner
return f
Then we set up our generator.
In [ ]:
from ipywidgets import IntSlider, VBox, HTML
slider2=IntSlider()
@yield_for_change(slider2, 'value')
def f():
for i in range(10):
print('did work %s'%i)
x = yield
print('generator function continued with value %s'%x)
f()
slider2